#include <cassert>
#include <fstream>
#include <GLES2/gl2.h>
#include <GLES3/gl31.h>
#include <GLES2/gl2ext.h>

#include "common/os.hpp"
#include "context.hpp"
#include "fbo.hpp"
#include "render_target.hpp"
#include "vbo.hpp"

#include "dump.hpp"

namespace
{

UInt32 GetTextureTarget(UInt32 t)
{
    if (t == GL_TEXTURE_2D)
        return GL_TEXTURE_2D;
    else if (t == GL_TEXTURE_CUBE_MAP ||
             t == GL_TEXTURE_CUBE_MAP_POSITIVE_X ||
             t == GL_TEXTURE_CUBE_MAP_NEGATIVE_X ||
             t == GL_TEXTURE_CUBE_MAP_POSITIVE_Y ||
             t == GL_TEXTURE_CUBE_MAP_NEGATIVE_Y ||
             t == GL_TEXTURE_CUBE_MAP_POSITIVE_Z ||
             t == GL_TEXTURE_CUBE_MAP_NEGATIVE_Z)
        return GL_TEXTURE_CUBE_MAP;
    else
    {
        PAT_DEBUG_ASSERT(0, "Should not be here\n");
        return GL_NONE;
    }
}

}

namespace pat
{

/////////////////////////////////////////////////////////////////
// Global functions to access state manager for each thread
/////////////////////////////////////////////////////////////////

typedef std::map<UInt32, ContextPtr> ContextThreadMap;
static ContextThreadMap gContexts;

void InitializeContexts()
{
}

void UninitializeContexts()
{
    gContexts.clear();
}

ContextPtr GetStateMangerForThread(UInt32 threadID)
{
    ContextThreadMap::iterator found = gContexts.find(threadID);
    if (found == gContexts.end())
    {
        ContextPtr ptr(new Context);
        gContexts[threadID] = ptr;
        return ptr;
    }
    else
    {
        return found->second;
    }
}

const ContextPtrList GetAllContexts()
{
    ContextPtrList contexts;
    ContextThreadMap::const_iterator citer = gContexts.begin();
    for (; citer != gContexts.end(); ++citer)
    {
        contexts.push_back(citer->second);
    }
    return contexts;
}

/////////////////////////////////////////////////////////////////
// Initialization & Uninitialization
/////////////////////////////////////////////////////////////////

Context::Context()
{
    Initialize();
}

Context::~Context()
{
    // In some cases, eglTerminate will not be called
    Uninitialize();
}

void Context::SetCurrentCallNumber(UInt32 cn)
{
    _currentCallNumber = cn;
}

UInt32 Context::GetCurrentCallNumber() const
{
    return _currentCallNumber;
}

void Context::Initialize()
{
    Reset();
}

void Context::Uninitialize()
{
    Reset();
}

void Context::Reset()
{
    _currentCallNumber = 0;

    _activeTextureUnit = GL_TEXTURE0;
}

/////////////////////////////////////////////////////////////////
// Framebuffer Object
/////////////////////////////////////////////////////////////////

//RESOURCE_MANAGEMENT_DEFINITION(FramebufferObject, framebufferObject)

void Context::BindTextureObjectToFramebufferObject(UInt32 target, UInt32 attachment, UInt32 textureHandle, UInt32 textureLevel)
{
    TextureObjectPtr tex = FindTextureObject(textureHandle, _currentCallNumber);
    if (tex)
    {
        tex->BoundToFramebufferObject();
    }
}

/////////////////////////////////////////////////////////////////
// Texture Object
/////////////////////////////////////////////////////////////////

void Context::SetActiveTextureUnit(UInt32 unit)
{
    _activeTextureUnit = unit;
}

UInt32 Context::GetActiveTextureUnit() const
{
    return _activeTextureUnit;
}

void Context::CreateTextureObject(UInt32 handle)
{
    TextureObjectPtr tex(new TextureObject);
    PAT_DEBUG_ASSERT_NEW(tex);

    _textureObjects[ObjectID(handle, _currentCallNumber)] = tex;
}

TextureObjectPtr Context::FindTextureObject(UInt32 handle, UInt32 callNum)
{
    if (_textureObjects.size() != 0)
    {
        ObjectID oid(handle, callNum);
        TextureObjectMap::iterator upper_bound = _textureObjects.upper_bound(oid);
        if (upper_bound != _textureObjects.begin())
        {
            --upper_bound;
            const ObjectID &id = upper_bound->first;
            if (id.handle == handle && id.callNumber <= callNum)
                return upper_bound->second;
        }
    }
    return TextureObjectPtr();
}

bool Context::BindTextureObject(UInt32 target, UInt32 handle)
{
    TextureObjectPtr tex = FindTextureObject(handle, _currentCallNumber);
    if (tex)
    {
        _boundTextureObjects[BoundTextureObjectKey(_activeTextureUnit, target)] = tex;
        return true;
    }
    else
    {
        // The texture ID has not been generated by glGenTextures, but fine to bind with;
        // As long as it has not been used by other textures yet.
        TextureObjectPtr tex(new TextureObject);
        PAT_DEBUG_ASSERT_NEW(tex);

        _textureObjects[ObjectID(handle, _currentCallNumber)] = tex;
        _boundTextureObjects[BoundTextureObjectKey(_activeTextureUnit, target)] = tex;
        return true;
    }
}

TextureObjectPtr Context::GetBoundTextureObject(UInt32 unit, UInt32 target)
{
    target = GetTextureTarget(target);
    BoundTextureObjectMap::iterator iter = _boundTextureObjects.find(BoundTextureObjectKey(unit, target));
    if (iter != _boundTextureObjects.end())
    {
        return iter->second;
    }
    else
    {
        return TextureObjectPtr();
    }
}

void Context::SetTexImage(UInt32 target, UInt32 level, UInt32 format, UInt32 type, UInt32 width, UInt32 height, const void *data)
{
    TextureObjectPtr tex = GetBoundTextureObject(GetActiveTextureUnit(), GetTextureTarget(target));
    PAT_DEBUG_ASSERT_POINTER(tex);
    if (tex) tex->SetImage(target, level, format, type, width, height, data);
}

void Context::SetTexSubImage(UInt32 target, UInt32 level, UInt32 format, UInt32 type, UInt32 xoffset, UInt32 yoffset, UInt32 width, UInt32 height, const void *data)
{
    TextureObjectPtr tex = GetBoundTextureObject(GetActiveTextureUnit(), GetTextureTarget(target));
    PAT_DEBUG_ASSERT_POINTER(tex);
    if (tex) tex->SetSubImage(target, level, format, type, xoffset, yoffset, width, height, data);
}

void Context::SetCompressedTexImage(UInt32 target, UInt32 level, UInt32 format, UInt32 width, UInt32 height, UInt32 dataSize, const void *data)
{
    TextureObjectPtr tex = GetBoundTextureObject(GetActiveTextureUnit(), GetTextureTarget(target));
    PAT_DEBUG_ASSERT_POINTER(tex);
    if (tex) tex->SetCompressedImage(target, level, format, width, height, data, dataSize);
}

void Context::GenerateMipmap(UInt32 target)
{
    TextureObjectPtr tex = GetBoundTextureObject(GetActiveTextureUnit(), target);
    PAT_DEBUG_ASSERT_POINTER(tex);
    if (tex) tex->GenerateMipmap();
}

void Context::DumpTextureObjects() const
{
    const std::string header = "thread,handle,callNum,width,height,format,type,dataSize";
    std::ofstream of("TextureObjects.csv");
    of << header << std::endl;

    TextureObjectMap::const_iterator citer = _textureObjects.begin();
    for (; citer != _textureObjects.end(); ++citer)
    {
        const TextureObjectPtr tex = citer->second;
        tex->Dump(citer->first, of);
    }
}

/////////////////////////////////////////////////////////////////
// Shader Objects
/////////////////////////////////////////////////////////////////

void Context::CreateShader(UInt32 shaderType, UInt32 name)
{
    ShaderObjectPtr shader(new ShaderObject);
    PAT_DEBUG_ASSERT_NEW(shader);
    shader->type = shaderType;

    _shaderObjects[ObjectID(name, _currentCallNumber)] = shader;
}

void Context::ShaderSource(UInt32 name, const std::string &source)
{
    ShaderObjectPtr obj = FindShaderObject(name, _currentCallNumber);
    PAT_DEBUG_ASSERT(obj, "Shader object %d don't exist currently. [CallNumber : %d]\n", name, _currentCallNumber);

    if (obj)
    {
        obj->source = source;
    }
}

void Context::DumpShaderObjects() const
{
    ShaderObjectMap::const_iterator citer = _shaderObjects.begin();
    for (; citer != _shaderObjects.end(); ++citer)
    {
        char buffer[256];
        if (citer->second->type == GL_VERTEX_SHADER)
        {
            sprintf(buffer, "ShaderObject%04d_Call%08d.vert", citer->first.handle, citer->first.callNumber);
        }
        else if (citer->second->type == GL_FRAGMENT_SHADER)
        {
            sprintf(buffer, "ShaderObject%04d_Call%08d.frag", citer->first.handle, citer->first.callNumber);
        }
        else if (citer->second->type == GL_COMPUTE_SHADER)
        {
            sprintf(buffer, "ShaderObject%04d_Call%08d.comp", citer->first.handle, citer->first.callNumber);
        }
        else if (citer->second->type == GL_GEOMETRY_SHADER_EXT)
        {
            sprintf(buffer, "ShaderObject%04d_Call%08d.geom", citer->first.handle, citer->first.callNumber);
        }
        else if (citer->second->type == GL_TESS_CONTROL_SHADER_EXT)
        {
            sprintf(buffer, "ShaderObject%04d_Call%08d.tesc", citer->first.handle, citer->first.callNumber);
        }
        else if (citer->second->type == GL_TESS_EVALUATION_SHADER_EXT)
        {
            sprintf(buffer, "ShaderObject%04d_Call%08d.tese", citer->first.handle, citer->first.callNumber);
        }
        else
        {
            DBG_LOG("Unknown shader type encountered for shader %04d: 0x%04x\n", citer->first.handle, citer->second->type);
            sprintf(buffer, "ShaderObject%04d_Call%08d.unknown", citer->first.handle, citer->first.callNumber);
        }

        std::ofstream of(buffer);
        of << citer->second->source;
        of.close();
    }
}

ShaderObjectPtr Context::FindShaderObject(UInt32 handle, UInt32 callNum)
{
    if (_shaderObjects.size() != 0)
    {
        ObjectID oid(handle, callNum);
        ShaderObjectMap::iterator upper_bound = _shaderObjects.upper_bound(oid);
        if (upper_bound != _shaderObjects.begin())
        {
            --upper_bound;
            const ObjectID &id = upper_bound->first;
            if (id.handle == handle && id.callNumber <= callNum)
                return upper_bound->second;
        }
    }
    return ShaderObjectPtr();
}

}
